<?php
/**
 * 微信
 * User: test
 * Date: 2019/7/3
 * Time: 19:08
 */

namespace app\common\lib;


use app\exception\IndexException;
use WeChatPay\Builder;
use WeChatPay\Crypto\AesGcm;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Formatter;
use WeChatPay\Util\PemUtil;

class Wechat
{
    protected $config = [];
    protected $wechatConfig = [];
    protected $weixinOpenRequestLogModel;
    protected static $instance;

    public function __construct()
    {
        $this->config = config('param.weixin_open');
        $this->wechatConfig = config('param.weixin_wechat');
        $this->weixinOpenRequestLogModel = \app\model\weixin\WeixinOpenRequestLog::getInstance();
    }

    public static function getInstance()
    {
        //        if(is_object(self::$instance)){
//            return self::$instance;
//        }


        return new self();
    }

    //通过code获取access_token
    public function getAccessTokenByCode($code)
    {
        if(empty($code)){
            throw new IndexException(4020,'code不能为空');
        }

        $url = 'https://api.weixin.qq.com/sns/oauth2/access_token';
        $param = [
            'appid'=>$this->config['appid'],
            'secret'=>$this->config['secret'],
            'code'=>$code,
            'grant_type'=>'authorization_code',
        ];

        $result = Common::curlApi($url,$param,false);

        $this->weixinOpenRequestLogModel->insertData($url,$param,$result,'通过code获取access_token');

        if(empty($result['access_token'])){
            throw new IndexException(4020,'access_token获取失败');
        }

        return $result;
    }

    //刷新 access_token 有效期
    public function refreshToken($refreshToken)
    {
        if(empty($refreshToken)){
            throw new IndexException(4020,'refreshtoken不能为空');
        }

        $url = 'https://api.weixin.qq.com/sns/oauth2/refresh_token';
        $param = [
            'appid'=>$this->config['appid'],
            'refresh_token'=>$refreshToken,
            'grant_type'=>'refresh_token',
        ];

        $result = Common::curlApi($url,$param,false);

        $this->weixinOpenRequestLogModel->insertData($url,$param,$result,'刷新 access_token 有效期');

        if(empty($result['access_token'])){
            throw new IndexException(4020,'access_token获取失败');
        }

        return $result;
    }


    //获取用户个人信息（UnionID 机制）
    public function getUserInfoByOpenID($accessToken, $openid)
    {
        if(empty($accessToken)){
            throw new IndexException(4020,'accessToken不能为空');
        }
        if(empty($openid)){
            throw new IndexException(4020,'openid不能为空');
        }

        $url = 'https://api.weixin.qq.com/sns/userinfo';
        $param = [
            'access_token'=>$accessToken,
            'openid'=>$openid,
            'lang'=>'zh_CN',
        ];

        $result = Common::curlApi($url,$param,false);

        $this->weixinOpenRequestLogModel->insertData($url,$param,$result,'获取用户个人信息（UnionID 机制）');

        if(empty($result['openid'])){
            throw new IndexException(4020,'用户个人信息获取失败');
        }

        return $result;
    }

    //小程序通过code获取openid
    public function getOpenIdByCode($code)
    {
        if(empty($code)){
            throw new IndexException(4020,'code不能为空');
        }

        $url = 'https://api.weixin.qq.com/sns/jscode2session';
        $param = [
            'appid'=>$this->wechatConfig['appid'],
            'secret'=>$this->wechatConfig['secret'],
            'js_code'=>$code,
            'grant_type'=>'authorization_code',
        ];

        $result = Common::curlApi($url,$param,false);

        $this->weixinOpenRequestLogModel->insertData($url,$param,$result,'小程序通过code获取openid');

        if(empty($result['openid'])){
            throw new IndexException(4020,'openid获取失败');
        }

        return $result;
    }

    //小程序通过code获取openid
    public function getMobileByCode($code)
    {
        if(empty($code)){
            throw new IndexException(4020,'code不能为空');
        }
        $accessToken = $this->getAccessToken();
        $url = 'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token='.$accessToken;
        $param = [
            'code'=>$code,
        ];

        $result = Common::curlApi($url,$param,true);

        $this->weixinOpenRequestLogModel->insertData($url,$param,$result,'小程序通过code获取openid');

        if(empty($result['phone_info'])){
            throw new IndexException(4020,'手机号获取失败');
        }

        return $result['phone_info'];
    }

    public function getAccessToken()
    {
        $accessToken = cache()->get('miniprogram_access_token');
        if($accessToken){
            return $accessToken;
        }

        $url = 'https://api.weixin.qq.com/cgi-bin/token';
        $param = [
            'appid'=>$this->wechatConfig['appid'],
            'secret'=>$this->wechatConfig['secret'],
            'grant_type'=>'client_credential',
        ];

        $result = Common::curlApi($url,$param,false);

        $this->weixinOpenRequestLogModel->insertData($url,$param,$result,'小程序获取access_token');

        if(empty($result['access_token'])){
            throw new IndexException(2008,'access_token获取失败');
        }

        cache()->put('miniprogram_access_token',$result['access_token'],6000);

        return $result['access_token'];
    }

    /**
     * 发送订阅消息
     * @param $openid
     * @param $templateId
     * @param $data
     * @param string $page
     * @return bool
     * @throws IndexException
     */
    public function sendSubscribeMessage($openid,$templateId,$data,$page='')
    {
        $accessToken = $this->getAccessToken();

        $param = [
            'touser'=>$openid,
            'template_id'=>$templateId,
            'page'=>$page??'',
            'data'=>$data,
            'miniprogram_state'=>config('app.env')=='prod'?'formal':'trial'
        ];

        $header = [
            "Content-Type: application/json; charset=utf-8"
        ];
        $res = Common::curlApi('https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token='.$accessToken,$param,true,$header);

        if($res['errcode']>0){
            //file_put_contents('send_ship_order.log',json_encode($res,256));
            //throw new IndexException(2008,json_encode($res,256));
            return false;
        }

        return true;
    }

    public function decryptData($param)
    {
        require dirname(ROOT_PATH).'/framework/require/wechatAes/wxBizDataCrypt.php';

        $pc = new \WXBizDataCrypt($this->wechatConfig['appid'], $param['session_key']);
        $errCode = $pc->decryptData($param['encrypted_data'], $param['iv'], $data );

        return $data;
    }

    public function weChatPay($param)
    {
        try {
            // 从本地文件中加载「商户API私钥」，「商户API私钥」会用来生成请求的签名
            $merchantPrivateKeyFilePath = 'file://'.runtime_path('weixinpay/apiclient_key.pem');
            $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);

            // 「商户API证书」的「证书序列号」
            $merchantCertificateSerial = $this->wechatConfig['serial'] ?? '';

            // 从本地文件中加载「微信支付平台证书」，用来验证微信支付应答的签名
            $platformCertificateFilePath = 'file://'.runtime_path('weixinpay/wechat_pay_cert.pem');
            $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);

            // 从「微信支付平台证书」中获取「证书序列号」
            $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);

            // 构造一个 APIv3 客户端实例
            $instance = Builder::factory([
                'mchid'      => $this->wechatConfig['mchid'] ?? '', //商户号,
                'serial'     => $merchantCertificateSerial,
                'privateKey' => $merchantPrivateKeyInstance,
                'certs'      => [
                    $platformCertificateSerial => $platformPublicKeyInstance,
                ],
            ]);


            $resp = $instance
                ->chain('v3/pay/transactions/jsapi')
                ->post(['json' => [
                    'mchid'        => $this->wechatConfig['mchid'] ?? '',
                    'out_trade_no' => $param['out_trade_no'],
                    'appid'        => $this->wechatConfig['appid'],
                    'description'  => (string) ($param['description']??'商家收费'),
                    'notify_url'   => (string) rtrim(config('app.app_url'),'/').'/index/notify/wechatpaynotify',
                    'amount'       => [
                        'total'    => (int)($param['amount']*100),
                        'currency' => 'CNY'
                    ],
                    'payer'=>[
                        'openid'   => $param['openid']??''
                    ]
                ]]);

            $data = json_decode($resp->getBody(), true);
            return $data;
        } catch (\Exception $e) {
            throw $e;
        }
    }

    public function appPay($param)
    {
        try {
            // 从本地文件中加载「商户API私钥」，「商户API私钥」会用来生成请求的签名
            $merchantPrivateKeyFilePath = 'file://'.runtime_path('weixinpay/apiclient_key.pem');
            $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);

            // 「商户API证书」的「证书序列号」
            $merchantCertificateSerial = $this->wechatConfig['serial'] ?? '';

            // 从本地文件中加载「微信支付平台证书」，用来验证微信支付应答的签名
            $platformCertificateFilePath = 'file://'.runtime_path('weixinpay/wechat_pay_cert.pem');
            $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);

            // 从「微信支付平台证书」中获取「证书序列号」
            $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);

            // 构造一个 APIv3 客户端实例
            $instance = Builder::factory([
                'mchid'      => $this->wechatConfig['mchid'] ?? '', //商户号,
                'serial'     => $merchantCertificateSerial,
                'privateKey' => $merchantPrivateKeyInstance,
                'certs'      => [
                    $platformCertificateSerial => $platformPublicKeyInstance,
                ],
            ]);

            $jsonParam = [
                'appid'        => (string) $this->wechatConfig['appid'],
                'mchid'        => (string) ($this->wechatConfig['mchid'] ?? ''),
                'out_trade_no' => (string) $param['out_trade_no'],

                'description'  => (string) ($param['description']??'商家收费'),
                'notify_url'   => (string) rtrim(config('app.app_url'),'/').'/index/notify/wechatpaynotify',
                'amount'       => [
                    'total'    => (int)($param['amount']*100),
                    'currency' => 'CNY'
                ]
            ];

            $resp = $instance
                ->chain('v3/pay/transactions/app')
                ->post(['json' => $jsonParam]);

            $data = json_decode($resp->getBody(), true);
            return $data;
        } catch (\Throwable $e) {
            throw $e;
        }
    }

    public function getWechatPaySignAndParam($param){
        $params = [
            'appid'=>(string)$this->wechatConfig['appid'],
            'timeStamp'=>(string)time(),
            'nonceStr'=>(string)(time().Common::randString(6,1)),
            'package'=>'prepay_id='.$param['prepay_id'],
        ];
        $string = '';
        foreach ($params as $val){
            $string .= $val."\n";
        }

        $privateKey = file_get_contents(runtime_path('weixinpay/apiclient_key.pem'));

        $key = openssl_get_privatekey($privateKey);
        openssl_sign($string, $signature, $key, "SHA256");
        openssl_free_key($key);

        $params['paySign'] = base64_encode($signature);

        $params['signType'] = 'RSA';
        return $params;
    }

    public function getAppPaySignAndParam($prepayId){
        $params = [
            'appid'=>(string)$this->wechatConfig['appid'],
            'timestamp'=>(string)time(),
            'noncestr'=>(string)(Common::getRequestId()),
            'prepayid'=>(string)$prepayId,
        ];
        $string = '';
        foreach ($params as $val){
            $string .= $val."\n";
        }

        $privateKey = file_get_contents(runtime_path('weixinpay/apiclient_key.pem'));

        $key = openssl_get_privatekey($privateKey);
        openssl_sign($string, $signature, $key, "SHA256");
        openssl_free_key($key);

        $params['sign'] = base64_encode($signature);

        $params['partnerid'] = (string)$this->wechatConfig['mchid'];
        $params['package'] = (string)'Sign=WXPay';
        return $params;
    }

    public function wechatPayNotify()
    {
        $inWechatpaySignature = request()->header('wechatpay-signature');
        $inWechatpayTimestamp = request()->header('wechatpay-timestamp');
        $inWechatpaySerial = request()->header('wechatpay-serial');
        $inWechatpayNonce = request()->header('wechatpay-nonce');
        $inBody = request()->rawBody();// 请根据实际情况获取，例如: file_get_contents('php://input');

        $apiv3Key = $this->wechatConfig['apiv3_key'];// 在商户平台上设置的APIv3密钥

        // 根据通知的平台证书序列号，查询本地平台证书文件，
        // 假定为 `/path/to/wechatpay/inWechatpaySerial.pem`
        $platformPublicKeyInstance = Rsa::from('file://'.runtime_path('weixinpay/wechat_pay_cert.pem'), Rsa::KEY_TYPE_PUBLIC);

        // 检查通知时间偏移量，允许5分钟之内的偏移
        $timeOffsetStatus = 3000 >= abs(Formatter::timestamp() - (int)$inWechatpayTimestamp);
        $verifiedStatus = Rsa::verify(
        // 构造验签名串
            Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $inBody),
            $inWechatpaySignature,
            $platformPublicKeyInstance
        );

        if ($timeOffsetStatus && $verifiedStatus) {
            // 转换通知的JSON文本消息为PHP Array数组
            $inBodyArray = (array)json_decode($inBody, true);
            // 使用PHP7的数据解构语法，从Array中解构并赋值变量
            ['resource' => [
                'ciphertext'      => $ciphertext,
                'nonce'           => $nonce,
                'associated_data' => $aad
            ]] = $inBodyArray;
            // 加密文本消息解密
            $inBodyResource = AesGcm::decrypt($ciphertext, $apiv3Key, $nonce, $aad);
            // 把解密后的文本转换为PHP Array数组
            $inBodyResourceArray = (array)json_decode($inBodyResource, true);
            //print_r($inBodyResourceArray);// 打印解密后的结果

            return $inBodyResourceArray;
        }

        return [];
    }
}